package org.example.myprojectbackend.utils;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.util.Calendar;
import java.util.Date;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * @ClassName JwtUtils
 * @Author liupanpan
 * @Date 2025/1/3
 * @Description 用于处理jwt令牌的工具类
 */
@Component
public class JwtUtils {
    //给jwt令牌签名校验的秘钥
    @Value("${spring.security.jwt.key}")
    String key;
    //令牌过期时间，以小时为单位
    @Value("${spring.security.jwt.expire}")
    int expire;

    @Resource
    StringRedisTemplate template;

    //创建jwt
    /**
     * 根据UserDetails生成对应的jwt令牌
     * @param user
     * @param id
     * @param username
     * @return 令牌
     */
    public String createJwt(UserDetails user,int id, String username) {
        //加密算法
        Algorithm algorithm = Algorithm.HMAC256(key);
        Date expireDate = this.getExpireDate();
        return JWT.create()
                .withJWTId(UUID.randomUUID().toString())
                .withClaim("id",id)
                .withClaim("username",username)
                //从用户的权限集合中提取出所有权限的字符串表示，并将它们收集到一个 List 中。
                .withClaim("authorities",user.getAuthorities().stream().map(GrantedAuthority::getAuthority).toList())
                .withExpiresAt(expireDate)   //设置过期时间
                .withIssuedAt(new Date())  //token的颁发时间
                .sign(algorithm); // 签名
    }

    public Date getExpireDate() {
        //当前日期时间
        Calendar calendar = Calendar.getInstance();
        //将 expire * 24 小时添加到 calendar 实例中
        calendar.add(Calendar.HOUR,expire * 24);
        return calendar.getTime();
    }

    /**
     * 解析jwt令牌
     * @param headerToken 请求头中携带的token
     * @return DecodedJWT
     */
    public DecodedJWT verifyJwt(String headerToken) {
        //校验并转换请求头中的Token令牌
        String token = convertToken(headerToken);
        if (token == null) {
            return null;
        }
        Algorithm algorithm = Algorithm.HMAC256(key);
        //解析token
        //创建一个 JWT 验证器，使用指定的算法
        JWTVerifier verifier = JWT.require(algorithm).build();
        //验证token是否合法
        try {
            //验证给定的 token 是否有效，如果有效，将返回一个 DecodedJWT 对象；如果无效，将抛出异常。
            DecodedJWT jwt = verifier.verify(token);
            //判断token是否在黑名单
            if (this.isInvalidToken(jwt.getId())) return null;
//            Map<String, Claim> claims = jwt.getClaims();
            //判断令牌是否过期
            return new Date().after(jwt.getExpiresAt()) ? null : jwt;
        } catch (JWTVerificationException e) {
            return null;
        }


    }

    //token提取

    /**
     * 校验并转换请求头中的token令牌
     * @param headerToken 请求头中的token
     * @return 转换后的令牌
     */
    public String convertToken(String headerToken) {
        if (headerToken == null || !headerToken.startsWith("Bearer ")) {
            return null;
        }
        return headerToken.substring(7);
    }

    //把jwt存的信息解析出来
    /**
     * 将jwt对象中的内容封装为UserDetails
     * @param jwt 已解析的jwt对象
     * @return UserDetails
     */
    public UserDetails toUserDetails(DecodedJWT jwt) {
        //Claims 是 JWT 的有效载荷中的部分，通常用于存储用户的信息、权限等数据
        /**
         * jwt.getClaims() 方法用于从 DecodedJWT 对象中提取所有声明，并以 Map<String, Claim> 的形式返回。
         * 这里的 String 是声明的键（即声明的名称），而 Claim 是一个表示声明值的对象
         */
        Map<String, Claim> claims = jwt.getClaims();

        /**
         * 使用从 JWT 中提取的声明构建一个 User 对象：
         * claims.get("name").asString()：获取名为 "name" 的声明，并将其转换为字符串，作为用户名。
         * .password("")：设置用户密码为一个空字符串（通常在使用 JWT 时不需要存储密码）。
         * .authorities(claims.get("authorization").asArray(String.class))：获取名为 "authorization" 的声明，并将其作为权限数组传递给用户构造器。
         */
        return User
                .withUsername(claims.get("username").asString())
                .password("")
                //这个方法调用表示将获取到的数据转换为 String 类型的数组
                .authorities(claims.get("authorities").asArray(String.class))
                .build();
    }

    /**
     * 将jwt对象中的用户id提取出来
     * @param jwt 已解析的jwt对象
     * @return 用户id
     */
    public Integer toId(DecodedJWT jwt) {
        Map<String, Claim> claims = jwt.getClaims();
        return claims.get("id").asInt();
    }

    /**
     * 让指定Jwt令牌失效
     * @param headerToken 请求头中携带的令牌
     * @return 是否操作成功
     */
    public boolean invalidateJwt(String headerToken) {
        String token = this.convertToken(headerToken);
        Algorithm algorithm = Algorithm.HMAC256(key);
        JWTVerifier verifier = JWT.require(algorithm).build();
        try {
            DecodedJWT verify = verifier.verify(token);
            //token验证通过,传入token的id和过期时间 ，加入黑名单
            return deleteJwt(verify.getId(),verify.getExpiresAt());
        } catch (JWTVerificationException e) {
            return false;
        }

    }

    /**
     * 将token列入Redis黑名单中
     * @param uuid 令牌ID
     * @param time 过期时间
     * @return 是否操作成功
     */
    private boolean deleteJwt(String uuid,Date time) {
        //判断是否已经在黑名单中
        Date now = new Date();
        //过期时间与当前时间的差值，和0比较，取两者最大值，如果时间差小于 0，返回 0；否则返回计算得到的时间差。
        long expire = Math.max(time.getTime() - now.getTime(), 0);
        /**
         * template.opsForValue().set(...): 使用Redis模板存储一个值。
         * Const.JWT_BLACK_LIST + uuid: 生成一个黑名单的键，通常是基于JWT的唯一标识符（UUID）。
         * "": 设置黑名单的值为空字符串，表示该JWT已被列入黑名单。
         * expire: 设置黑名单的过期时间(与token过期时间一致)，防止无限制的存储。
         * TimeUnit.MILLISECONDS: 指定时间单位为毫秒
         */
        template.opsForValue().set(Const.JWT_BLACK_LIST+uuid,"",expire, TimeUnit.MILLISECONDS);
        return true;
    }

    /**
     * 验证token是否被列入Redis黑名单
     * @param uuid 令牌ID
     * @return 是否操作成功
     */
    private boolean isInvalidToken(String uuid) {
        /*
        Boolean.TRUE 是一个常量，表示 true。
        equals 方法用于比较两个对象的值，避免了因直接比较可能导致的 NullPointerException。
        这种方式可以确保安全地判断一个布尔值是否为 true，即使它可能是 null。
         */
        return Boolean.TRUE.equals(template.hasKey(Const.JWT_BLACK_LIST+uuid));
    }

}
